﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UserScript.ExpressionParsing;
using UserScript.ExpressionParsing.Irony.Parsing;

namespace UserScript
{
    public class PrecompiledScript<T>
    {
        private readonly Func<T, string> _func;

        public static PrecompiledScript<T> Compile(string Template)
        {
            var arg = Expression.Parameter(typeof(T), "arg");

            var ret = ConcatStrings(GetExpressions(Template, arg));

            if (ret.CanReduce)
                ret = ret.ReduceAndCheck();

            var func = (Func<T, string>)Expression.Lambda(ret, arg).Compile();

            return new PrecompiledScript<T>(Template, func);
        }
        public static IEnumerable<Expression> GetExpressions(string Template)
        {
            var arg = Expression.Parameter(typeof(T), "arg");

            return GetExpressions(Template, arg);
        }

        public static IEnumerable<MethodCallExpression> GetExpressionsWith(string Template, MethodInfo method)
        {
            return GetExpressions(Template).SelectMany(c => GetExpressionsWith(c, method));
        }
        private static IEnumerable<MethodCallExpression> GetExpressionsWith(Expression parent, MethodInfo method)
        {
            if (parent is MethodCallExpression)
            {
                var methodCall = (MethodCallExpression) parent;

                if (methodCall.Method == method)
                    return new[] { methodCall };

                return methodCall.Arguments.SelectMany(c => GetExpressionsWith(c, method));
            }

            if (parent is NewExpression)
            {
                var newObj = (NewExpression)parent;

                return newObj.Arguments.SelectMany(c => GetExpressionsWith(c, method));
            }

            if (parent is SwitchExpression)
            {
                var typed = (SwitchExpression)parent;

                return typed.Cases.SelectMany(c => GetExpressionsWith(c.Body, method));
            }

            if (parent is TypeBinaryExpression)
            {
                var typed = (TypeBinaryExpression)parent;

                return GetExpressionsWith(typed.Expression, method);
            }

            if (parent is MemberInitExpression)
            {
                var typed = (MemberInitExpression)parent;

                return GetExpressionsWith(typed.NewExpression, method);
            }

            if (parent is LoopExpression)
            {
                var typed = (LoopExpression)parent;

                return GetExpressionsWith(typed.Body, method);
            }

            if (parent is LambdaExpression)
            {
                var typed = (LambdaExpression)parent;

                return GetExpressionsWith(typed.Body, method);
            }

            if (parent is InvocationExpression)
            {
                var typed = (InvocationExpression)parent;

                return GetExpressionsWith(typed.Expression, method).Concat(typed.Arguments.SelectMany(c => GetExpressionsWith(c, method)));
            }

            if (parent is ConditionalExpression)
            {
                var typed = (ConditionalExpression)parent;

                return GetExpressionsWith(typed.Test, method)
                    .Concat(GetExpressionsWith(typed.IfTrue, method))
                    .Concat(GetExpressionsWith(typed.IfFalse, method));
            }

            if (parent is BlockExpression)
            {
                var typed = (BlockExpression)parent;

                return GetExpressionsWith(typed.Result, method)
                    .Concat(typed.Expressions.SelectMany(c => GetExpressionsWith(c, method)));
            }

            if (parent is TryExpression)
            {
                var typed = (TryExpression)parent;

                return GetExpressionsWith(typed.Body, method)
                    .Concat(GetExpressionsWith(typed.Fault, method))
                    .Concat(GetExpressionsWith(typed.Finally, method));
            }
            
            if (parent is NewArrayExpression)
            {
                var newArray = (NewArrayExpression)parent;

                return newArray.Expressions.SelectMany(c => GetExpressionsWith(c, method));
            }

            if (parent is BinaryExpression)
            {
                var binary = (BinaryExpression)parent;

                return GetExpressionsWith(binary.Left, method).Concat(GetExpressionsWith(binary.Right, method));
            }

            return Enumerable.Empty<MethodCallExpression>();
        }

        private static IEnumerable<Expression> GetExpressions(string Template, ParameterExpression arg)
        {
            int pos = 0;
            int len = Template.Length;

            StringBuilder builder = new StringBuilder();

            while (true)
            {
                char ch;
                while (pos < len)
                {
                    ch = Template[pos];

                    pos++;
                    if (ch == '}')
                    {
                        if (pos < len && Template[pos] == '}') // Treat as escape character for }}
                            pos++;
                        else
                            throw new FormatException("An unescaped closing curly brace cannot precede an unescaped opening one.");
                    }
                    else if (ch == '{')
                    {
                        if (pos < len && Template[pos] == '{') // Treat as escape character for {{
                            pos++;
                        else
                        {
                            pos--;
                            break;
                        }
                    }

                    builder.Append(ch);
                }

                yield return Expression.Constant(builder.ToString());
                builder.Clear();

                if (pos == len) break;
                pos++;

                if (pos == len) throw new FormatException();

                bool inEscape = false, inQuotes = false;

                ch = Template[pos];
                do
                {
                    switch (ch)
                    {
                        case '\\':
                        {
                            if (inEscape)
                            {
                                inEscape = false;
                                goto default;
                            }
                            else
                            {
                                inEscape = true;
                                break;
                            }
                        }
                        case 't':
                        {
                            if (inEscape)
                            {
                                inEscape = false;
                                builder.Append('\t');
                                break;
                            }
                            else
                            {
                                goto default;
                            }
                        }
                        case 'r':
                        {
                            if (inEscape)
                            {
                                inEscape = false;
                                builder.Append('\r');
                                break;
                            }
                            else
                            {
                                goto default;
                            }
                        }
                        case 'n':
                        {
                            if (inEscape)
                            {
                                inEscape = false;
                                builder.Append('\n');
                                break;
                            }
                            else
                            {
                                goto default;
                            }
                        }
                        case '"':
                        {
                            if (inEscape)
                            {
                                builder.Append("\\\"");
                                inEscape = false;

                                break;
                            }
                            else
                            {
                                inQuotes = !inQuotes;

                                goto default;
                            }
                        }
                        default:
                        {
                            inEscape = false;

                            builder.Append(ch);

                            break;
                        }
                    }

                    pos++;
                    if (pos == len) throw new FormatException();
                    ch = Template[pos];
                }
                while (ch != '}' || inQuotes);

                pos++;

                var tree = ExpressionParser.ParseExpression(builder.ToString());

                if (tree.Status == ParseTreeStatus.Error)
                    throw new Exception("Parse error: " + tree.ParserMessages[0].Message);

                yield return Evaluate(tree.Root, arg);
                builder.Clear();
            }
        }

        private static Expression ConcatStrings(IEnumerable<Expression> exprs)
        {
            var concat = typeof(string).GetMethod("Concat", new[] { typeof(object[]) });
            return Expression.Call(concat, Expression.NewArrayInit(typeof(object), exprs.Select(c => ConvertIfNecessary(c, typeof(object)))));
        }

        private static readonly Dictionary<string, ExpressionType>  OpMap = new Dictionary<string, ExpressionType>()
        {
            {"==", ExpressionType.Equal},
            {"!=", ExpressionType.NotEqual},
            
            {"*", ExpressionType.Multiply},
            {"-", ExpressionType.Subtract},
            {"+", ExpressionType.Add},
            {"/", ExpressionType.Divide},
            {"%", ExpressionType.Modulo},
            
            {"<>", ExpressionType.NotEqual},

            {"<", ExpressionType.LessThan},
            {"<=", ExpressionType.LessThanOrEqual},

            {">", ExpressionType.GreaterThan},
            {">=", ExpressionType.GreaterThanOrEqual},
            
            {"&&", ExpressionType.AndAlso},
            {"||", ExpressionType.OrElse},
        };

        private static Expression Evaluate(ParseTreeNode node, ParameterExpression arg)
        {
            switch (node.Term.Name)
            {
                case "id_simple":
                {
                    string id = node.Token.Value.ToString();

                    var prop = arg.Type.GetProperties().FirstOrDefault(c => string.Equals(c.Name, id, StringComparison.CurrentCultureIgnoreCase));

                    if (prop == null)
                        throw new Exception("Unknown property!");

                    return Expression.Property(arg, prop);
                }
                case "number":
                case "string":
                {
                    return Expression.Constant(node.Token.Value);
                }
                case "NULL":
                {
                    return Expression.Constant(null);
                }
                case "binExpr":
                {
                    ExpressionType type;
                    if (!OpMap.TryGetValue(node.ChildNodes[1].ChildNodes[0].Term.Name, out type))
                    {
                        throw new NotSupportedException("Operator " + node.ChildNodes[1].ChildNodes[0].Term.Name + " is not supported.");
                    }

                    var left = Evaluate(node.ChildNodes[0], arg);
                    var right = Evaluate(node.ChildNodes[2], arg);

                    if (type == ExpressionType.Add && (left.Type == typeof(string) || right.Type == typeof(string)))
                    {
                        return ConcatStrings(new [] { left, right });
                    }

                    return Expression.MakeBinary(type, left, right);
                }
                case "unExpr":
                {
                    var right = Evaluate(node.ChildNodes[1], arg);

                    switch (node.ChildNodes[0].ChildNodes[0].Term.Name)
                    {
                        case "!":
                        {
                            return Expression.MakeUnary(ExpressionType.Not, right, right.Type);
                        }
                        case "-":
                        {
                            return Expression.MakeUnary(ExpressionType.Subtract, right, right.Type);
                        }
                    }

                    throw new NotSupportedException("unExpr \"" + node.ChildNodes[0].ChildNodes[0].Term.Name + "\" is not supported.");

                }
                case "funCall":
                {
                    var parameters = node.ChildNodes[1].ChildNodes.Select(c => Evaluate(c, arg)).ToArray();

                    var functionName = (string) node.ChildNodes[0].Token.Value;

                    var method = GetBestMethod(arg.Type, functionName, parameters.Select(c => c.Type));

                    if (method == null)
                    {
                        method = GetBestMethod(typeof(ExpressionFunctionExecutor), functionName, parameters.Select(c => c.Type));

                        if (method == null)
                        {
                            throw new Exception("Unknown method!");
                        }

                        return Expression.Call(method, parameters.Zip(method.GetParameters(), (param, Info) => ConvertIfNecessary(param, Info.ParameterType)));
                    }

                    return Expression.Call(arg, method, parameters.Zip(method.GetParameters(), (param, Info) => ConvertIfNecessary(param, Info.ParameterType)));
                }
            }

            throw new NotSupportedException(node.Term.Name + " is not supported.");
        }

        private static Expression ConvertIfNecessary(Expression expr, Type type)
        {
            if (expr.Type == type)
                return expr;

            return Expression.Convert(expr, type);
        }

        private static MethodInfo GetBestMethod(Type type, string name, IEnumerable<Type> parameters)
        {
            var sameName = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(c => string.Equals(c.Name, name, StringComparison.CurrentCultureIgnoreCase) ||
                                                                                                                         string.Equals("@" + c.Name, name, StringComparison.CurrentCultureIgnoreCase));

            var sameParams = sameName.Where(c => ExactMatch(c.GetParameters(), parameters, (Info, Type) => Info.ParameterType.IsAssignableFrom(Type)));

            var ret = sameParams.FirstOrDefault();

            return ret;
        }

        private static bool ExactMatch<T1, T2>(IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, bool> Match)
        {
            var enumerator1 = first.GetEnumerator();
            var enumerator2 = second.GetEnumerator();

            while (enumerator1.MoveNext())
            {
                if (!enumerator2.MoveNext())
                    return false;

                if (!Match(enumerator1.Current, enumerator2.Current))
                    return false;
            }

            if (enumerator2.MoveNext())
                return false;

            return true;
        }

        protected PrecompiledScript(string Template, Func<T, string> func)
        {
            this.Template = Template;
            this._func = func;
        }

        public string Template { get; set; }
        
        public string Execute(T Arg)
        {
            return this._func(Arg);
        }
    }
}
